---
title: "stop waiting for aws: self-host your dev stack on autopilot"
author: "pilput"
canonical: "https://pilput.net/pilput/stop-waiting-for-aws-self-host-your-dev-stack-on-autopilot"
published: "2025-09-06T04:04:05.806321Z"
updated: "2025-09-06T04:04:05.806321Z"
description: "why waiting for aws credits is killing your momentum every month, students post the same question on reddit: “ how long until aws educate credits arrive? ” mea..."
---
# stop waiting for aws: self-host your dev stack on autopilot

## why waiting for aws credits is killing your momentum

every month, students post the same question on reddit: “_how long until aws educate credits arrive?_” meanwhile, their side-projects sit untouched and portfolios stay empty. **devops is a hands-on sport**; you learn by wiring pieces together, not by reading docs on the bus. in this guide you’ll build a miniature, production-ready stack on a **$5/month vps** that you control completely—no more gatekeepers, no more expiration dates.

## what we’re about to build

a “dev stack on autopilot” that gives you:

- a git server (gitea) to host private repos

- a ci runner (drone) that tests and builds every push

- a container registry that keeps your docker images safe

- a deployment target (dokku) so you can `git push` straight to production

- free, auto-renewing ssl via let’s encrypt

all glued together with docker compose and watched by a tiny systemd service that reboots things if they crash. total ram footprint: < 400 mb.

## pick a $5 cloud and lock it down in 5 minutes

### 1. create the vm

any provider with ubuntu 22.04 ltc works—digitalocean, hetzner, linode. select the **ipv6-enabled** plan; we’ll use that later for extra nerd cred.

```
# ssh in, never use password auth again
ssh root@your-ipv4
```

### 2. harden the box

```
apt update && apt upgrade -y
adduser deploy && usermod -ag sudo deploy
curl -fssl https://github.com/.keys >> /home/deploy/.ssh/authorized_keys
# disable root login
sed -i 's/^permitrootlogin yes/permitrootlogin no/' /etc/ssh/sshd_config
systemctl restart sshd
```

### 3. point your domain

create three a records:

- `git.yourdomain.com` → your-server-ip

- `ci.yourdomain.com` → same ip

- `*.apps.yourdomain.com` → same ip (dokku will catch sub-domains)

## install docker and docker-compose (one-liner)

```
curl -fssl https://get.docker.com | sh
sudo usermod -ag docker deploy
su - deploy
mkdir -p ~/dev-stack && cd $_
# docker-compose v2 comes with the script above, verify:
docker compose version
```

## define the stack in a single docker-compose.yml

place this file at `~/dev-stack/docker-compose.yml`:

```
version: "3.9"
services:
  gitea:
    image: gitea/gitea:1.20-rootless
    environment:
      - gitea__server__root_url=https://git.${domain}
      - gitea__server__ssh_domain=git.${domain}
      - gitea__server__ssh_port=2222
    volumes:
      - ./gitea:/data
    ports:
      - "3000:3000"
      - "2222:2222"
    restart: unless-stopped

  drone:
    image: drone/drone:2
    environment:
      - drone_gitea_server=https://git.${domain}
      - drone_gitea_client_id=${drone_gitea_client_id}
      - drone_gitea_client_secret=${drone_gitea_client_secret}
      - drone_rpc_secret=${drone_rpc_secret}
      - drone_server_host=ci.${domain}
      - drone_server_proto=https
    volumes:
      - ./drone:/data
    ports:
      - "8080:80"
    restart: unless-stopped

  registry:
    image: registry:2
    environment:
      - registry_http_addr=0.0.0.0:5000
      - registry_storage_filesystem_rootdirectory=/data
    volumes:
      - ./registry:/data
    ports:
      - "5000:5000"
    restart: unless-stopped

  nginx-proxy:
    image: nginxproxy/nginx-proxy:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certs:/etc/nginx/certs
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./vhost.d:/etc/nginx/vhost.d
      - ./html:/usr/share/nginx/html
    restart: unless-stopped

  acme-companion:
    image: nginxproxy/acme-companion
    volumes:
      - ./certs:/etc/nginx/certs
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme:/etc/acme.sh
    volumes_from:
      - nginx-proxy
```

create a companion `.env` file—never commit secrets!

```
domain=yourdomain.com
drone_rpc_secret=$(openssl rand -hex 16)
# leave the two lines below empty for now
drone_gitea_client_id=
drone_gitea_client_secret=
```

## launch and close the ports

```
docker compose up -d
```

ufw example:

```
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
```

## wire gitea + drone (oauth dance)

- visit `https://git.yourdomain.com` → sign up → top-right avatar → site administration → applications → new oauth2 application.

- name: drone; redirect uri: `https://ci.yourdomain.com/login`; copy client id/secret into the `.env` file.

- restart drone: `docker compose restart drone`.

- hit `https://ci.yourdomain.com` and “authorize application”.

## push your first project (full-stack example)

we’ll deploy a tiny express + react repo so you see the whole pipeline.

### 1. create the repo in gitea

### 2. add a `.drone.yml`file at the root

```
kind: pipeline
type: docker
name: default

steps:
  - name: test
    image: node:18-alpine
    commands:
      - npm ci
      - npm test

  - name: build
    image: node:18-alpine
    commands:
      - npm run build
      - docker build -t registry.yourdomain.com/myapp:$${drone_commit_sha:0:8} .
      - docker push registry.yourdomain.com/myapp:$${drone_commit_sha:0:8}

image_pull_secrets:
  - dockerconfig
```

### 3. add a dockerfile (multi-stage keeps images tiny)

```
# ---- build stage ----
from node:18-alpine as build
workdir /app
copy package*.json ./
run npm ci
copy . .
run npm run build

# ---- run stage ----
from node:18-alpine
workdir /app
copy --from=build /app/dist ./dist
copy --from=build /app/node_modules ./node_modules
expose 3000
cmd ["node","dist/server.js"]
```

commit + push. drone should turn green inside 30 s, and your image now sits in the registry.

## install dokku (your personal heroku)

```
# run as root
wget https://raw.githubusercontent.com/dokku/dokku/v0.31.0/bootstrap.sh
sudo dokku_tag=v0.31.0 bash bootstrap.sh
```

visit `http://your-server-ip` once to paste your ssh public key. then, on your laptop:

```
# create the app
ssh dokku@yourdomain.com apps:create myapp
# link registry
ssh dokku@yourdomain.com registry:login registry.yourdomain.com
# set image
ssh dokku@yourdomain.com tags:deploy myapp registry.yourdomain.com/myapp:abc123
# enable let’s encrypt
ssh dokku@yourdomain.com letsencrypt:enable myapp
# done: https://myapp.apps.yourdomain.com is live
```

## autopilot hardening checklist

- turn on unattended-upgrades: `sudo apt install unattended-upgrades`

- backup gitea daily with `docker exec gitea gitea dump` → rsync to cheap object storage

- set up `logrotate` for docker; containers are noisy

- add a simple cron health-check:

```
*/5 * * * * /home/deploy/health-check.sh || /usr/bin/docker compose -f /home/deploy/dev-stack/docker-compose.yml restart
```

## cost & seo side effects

**devops wins**: by actually shipping you learn nginx reverse-proxy rules, let’s encrypt renewal hooks, oauth scopes—skills every employer asks for.

**seo wins**: owning your domain means every tutorial you publish lives at `awesome-post.yourdomain.com`, building domain authority instead of sending juice to medium/dev.to.

**full-stack wins**: frontend, backend, ci, registry, ssl—one horizontal slice you can demo in interviews.

    component
    ram
    monthly cost

    gitea
    120 mb
    $5 total

    drone
    80 mb

    registry
    40 mb

    dokku
    100 mb

    system overhead
    ~200 mb

## tl;dr for impatient students

waiting for aws credits is the new “my dog ate my homework.” spin up a $5 vps, paste the files above, and you’ll have a private git, ci, registry, and heroku-style deploy in under an hour. commit code tonight; show a working url tomorrow. **stop waiting for aws—start owning your devops journey today.**
