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)

  1. visit https://git.yourdomain.com → sign up → top-right avatar → site administration → applications → new oauth2 application.
  2. name: drone; redirect uri: https://ci.yourdomain.com/login; copy client id/secret into the .env file.
  3. restart drone: docker compose restart drone.
  4. 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.ymlfile 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 [email protected] apps:create myapp
# link registry
ssh [email protected] registry:login registry.yourdomain.com
# set image
ssh [email protected] tags:deploy myapp registry.yourdomain.com/myapp:abc123
# enable let’s encrypt
ssh [email protected] 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.

Comments

Discussion

Share your thoughts and join the conversation

Loading comments...

Join the Discussion

Please log in to share your thoughts and engage with the community.