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 pushstraight 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-ipci.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.envfile. - restart drone:
docker compose restart drone. - hit
https://ci.yourdomain.comand “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
logrotatefor 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
Share your thoughts and join the conversation
Loading comments...
Please log in to share your thoughts and engage with the community.