A self-hosted password manager running in Docker on an AWS EC2 instance, provisioned end-to-end with Terraform. No clicking through the console, no manual config drift.
Built this to get hands-on with cloud networking and IaC. Everything from the VPC to the running container is defined in code.
[ Local Arch Linux Terminal ]
│
▼ (Terraform apply)
┌──────────────────────────────────────────────────────┐
│ AWS (Singapore Region) (VPC: 10.0.0.0/16) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Public Subnet (10.0.1.0/24) │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ Security Group (Stateful Firewall) │ │ │
│ │ │ Ingress: 80/443 (HTTP/S), 22 (SSH) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ Ubuntu 24.04 EC2 Instance │ │ │ │
│ │ │ │ Docker runtime │ │ │ │
│ │ │ │ Persistent volume (/vw-data/) │ │ │ │
│ │ │ │ Vaultwarden container (TLS) │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Infrastructure as Code. Terraform provisions the VPC, subnet, route tables, internet gateway, security group, and EC2 instance. Run terraform apply, get a working server. No manual steps.
Automated bootstrap. A 'user_data" shell script runs on first boot to install Docker and start the container. The instance is ready to go without any SSH post-provisioning.
TLS inside the container. The ROCKET_TLS environment variable points Vaultwarden's Rocket backend at a certificate and key mounted into the container. Traffic is encrypted before it hits the application layer.
Locked-down firewall. Security group egress is restricted so the instance can pull packages and Docker images, but the inbound attack surface stays small.
The stuff that broke and how I fixed it.
What happened. Rapid browser reloads during setup — I essentially self-DDoS'd the instance — maxed out the default 1024 file descriptor limit faster than the kernel could clean up stale TCP connections.
Fix. Running the container with docker run -d detaches the process from the terminal entirely. The kernel can then properly clean up half-open TCP connections instead of letting them pile up against the limit.
What happened. Browsers refuse to run over plain HTTP. Bitwarden clients use this to encrypt vaults locally before sending anything to the server, so no HTTPS means no login.
Fix. I Generated a self-signed 4096-bit RSA certificate with OpenSSL directly on the instance. Then used SSH local port forwarding (-L 8080:localhost:443) to tunnel from my machine to the instance. The browser sees localhost, which satisfies the secure-context requirement without needing a public domain or a CA-signed certification.
- Terraform CLI
- AWS credentials configured (
aws configureor environment variables) - An SSH key pair at
~/.ssh/portfolio_key
# Download providers and initialize the working directory
terraform init
# Preview what Terraform will create
terraform plan
# Deploy to AWS Singapore
terraform apply -auto-approve# SSH in with your key
ssh -i ~/.ssh/portfolio_key ubuntu@<EC2_PUBLIC_IP># Generate a self-signed cert valid for 365 days
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodesdocker run -d \
--name vaultwarden \
-v /vw-data/:/data/ \
-v /home/ubuntu/cert.pem:/ssl/cert.pem \
-v /home/ubuntu/key.pem:/ssl/key.pem \
-e ROCKET_TLS='{certs="/ssl/cert.pem",key="/ssl/key.pem"}' \
-p 443:443 \
vaultwarden/server:latest# From your local machine
ssh -i ~/.ssh/portfolio_key -L 8080:localhost:443 ubuntu@<EC2_PUBLIC_IP> -N
# Then open in browser
https://localhost:8080
`
Accept the self-signed cert warning. The vault is running over an encrypted connection.
### 6. Tear down
```bash
terraform destroy -auto-approve
Run terraform destroy when you're done. Everything in the VPC gets removed.
- Terraform -- infrastructure provisioning
- AWS -- EC2, VPC, Security Groups, Internet Gateway
- Docker -- container runtime
- Vaultwarden -- Bitwarden-compatible self-hosted server (Rust/Rocket)
- OpenSSL -- self-signed TLS certificate generation
- Local (Arch Linux) -- where the Terraform code lives. Runs terraform apply, manages the SSH key pair (ed25519), and handles the port-forwarding tunnel.
- Cloud (Ubuntu 24.04, EC2 t3.micro) -- the Docker host. I picked Ubuntu over something leaner because I wanted a predictable base and didn't want to debug OS quirks on top of everything else. Vaultwarden runs detached with /vw-data/ mounted for persistence.