Persistent Docker Data on Amazon EFS: Mount It, Destroy Your EC2 Instance, and Recover Everything
A hands-on guide to storing Docker volumes and your docker-compose file on Amazon EFS instead of an EC2 instance’s disk, with SSM-only access. Includes a destroy-and-recover test, DBeaver access through SSM port forwarding, and the path to RDS.
Introduction
When you run containers on Amazon EC2, Docker volumes live on the instance’s own disk by default. If the instance is terminated, the data goes with it.
In this article I share what I built and learned: I provisioned an EC2 instance, created an Amazon EFS file system, and adapted the commands from the AWS tutorial so that Docker data (and even the
docker-compose.yml) lives on EFS. Then I terminated the instance, launched a new one, and recovered everything.The instance has no open inbound ports. All access goes through AWS Systems Manager (SSM) Session Manager.
What we will build
- An EC2 instance running Amazon Linux 2023, reachable only through SSM.
- An EFS file system mounted at
/mnt/efs. - A PostgreSQL container whose data and compose file are stored on EFS.
- A destroy-and-recover test.
- Local access from DBeaver through SSM port forwarding.
Step 1: Security groups and IAM
Create two security groups in the same VPC.
sg-ec2 (the instance)| Direction | Protocol / Port | Source or destination | Purpose |
|---|---|---|---|
| Inbound | None | n/a | No SSH (22), no open ports |
| Outbound | TCP 2049 | sg-efs | Talk to EFS (NFS) |
| Outbound | TCP 443 | 0.0.0.0/0 or VPC endpoints | SSM, yum, Docker image pulls |
sg-efs (the EFS mount targets)| Direction | Protocol / Port | Source | Purpose |
|---|---|---|---|
| Inbound | TCP 2049 | sg-ec2 | Only the instance can mount EFS |
Referencing a security group as the source, instead of an IP, keeps working even when you replace the instance.
For SSM to work you also need:
- An IAM role attached to the instance with the
AmazonSSMManagedInstanceCorepolicy. - Outbound HTTPS (443) to the SSM endpoints, through a NAT/Internet Gateway or VPC endpoints (
ssm,ssmmessages,ec2messages). - Amazon Linux 2023 already includes the SSM Agent.
Finally, create the EFS file system, add a mount target in the instance’s Availability Zone using
sg-efs, and copy the File system ID (fs-xxxxxxxx).Step 2: Connect with Session Manager and install EFS utilities
From the EC2 console, open the instance and choose Connect > Session Manager. Then run:
bash
1
2
sudo -i
yum install -y amazon-efs-utilsStep 3: Mount EFS
The AWS tutorial mounts into a folder in the current directory. I used a fixed, predictable path instead:
bash
1
2
mkdir -p /mnt/efs
mount -t efs -o tls fs-XXXXXXXX:/ /mnt/efsStep 4: Verify the mount
bash
1
2
3
4
5
df -h | grep efs
mount | grep efs
cd /mnt/efs
echo 'efs mounted in site A' >> efs-setup.log
cat efs-setup.log(Insert your screenshot here. Caption: “Output of
df -h and mount confirming EFS is mounted at /mnt/efs.” Blur the file system ID and hostname before uploading.)If EFS appears in
df and the log text prints back, the mount works.Step 5 (optional): Mount automatically after a reboot
An
/etc/fstab entry makes EFS mount again after a reboot of the same instance:bash
1
2
3
4
5
echo "fs-XXXXXXXX:/ /mnt/efs efs _netdev,tls 0 0" | tee -a /etc/fstab
cd ~
umount /mnt/efs
mount -a
df -h | grep efsTwo important notes:
- Leave the folder before unmounting. If your shell is inside
/mnt/efs,umountfails with “device is busy”, andmount -awill just report that it is already mounted. Runcd ~first. - fstab does not survive a terminated instance. It lives on the instance’s disk. In the destroy-and-recover test below, you mount manually again. To automate it for new instances, put the install and mount commands in your Terraform or launch template user data.
This step matters because of
restart: unless-stopped. If Docker starts at boot and EFS is not mounted yet, PostgreSQL would silently initialize a fresh database on the local disk, and it would look like you lost your data.Step 6: Install Docker and organize your folders on EFS
bash
1
2
3
4
5
6
7
8
yum install -y docker
systemctl enable --now docker
mkdir -p /usr/local/lib/docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
-o /usr/local/lib/docker/cli-plugins/docker-compose
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
docker compose versionHere is the technique that made the difference: store not only the data but also the
docker-compose.yml on EFS. This is the layout I used:text
1
2
3
4
/mnt/efs/
|- app/ docker-compose.yml
|- postgres-data/
|- backups/Create it:
bash
1
2
3
mkdir -p /mnt/efs/app /mnt/efs/postgres-data /mnt/efs/backups
cd /mnt/efs/app
nano docker-compose.ymlyaml
1
2
3
4
5
6
7
8
9
10
11
12
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: change_this_password
POSTGRES_DB: appdb
ports:
- "127.0.0.1:5432:5432"
volumes:
- /mnt/efs/postgres-data:/var/lib/postgresql/dataTwo details worth explaining:
- I use a bind mount to a path on EFS instead of a regular Docker volume, so the database files are written to EFS and not to the instance’s disk.
- The port is published on
127.0.0.1only. Nothing is exposed to the network, which fits our SSM-only design.
Start it:
bash
1
2
docker compose up -d
docker psStep 7: Create test data
bash
1
2
3
4
docker compose exec db psql -U app -d appdb -c \
"CREATE TABLE test (id serial PRIMARY KEY, message text);
INSERT INTO test (message) VALUES ('data stored on EFS');
SELECT * FROM test;"Step 8: Connect from DBeaver without opening any port
Since the security group has no inbound rules, we use SSM port forwarding to create a secure tunnel from your computer.
Requirements on your machine:
- AWS CLI configured (you probably have it if you use Terraform).
- The Session Manager plugin. On Windows, install it from the official AWS installer (
SessionManagerPluginSetup.exe, listed in the AWS documentation under “Install the Session Manager plugin”), then open a new terminal and runsession-manager-pluginto verify. On macOS:brew install --cask session-manager-plugin. - IAM permission
ssm:StartSessionfor the instance and theAWS-StartPortForwardingSessiondocument.
Open the tunnel and leave the terminal open:
bash
1
2
3
aws ssm start-session --target i-XXXXXXXX \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}'If your computer already uses port 5432, change
localPortNumber to 5433.In --target add the id of your instance.
In DBeaver, create a new PostgreSQL connection:
- Host:
localhost - Port:
5432(or5433) - Database:
appdb - User and password: the ones from your compose file
Click Test Connection. You can now query the database and run backups from a professional client, with zero open ports.
You can also back up directly from the instance and store the dump on EFS:
bash
1
docker compose exec db pg_dump -U app appdb > /mnt/efs/backups/appdb.sqlStep 9: The real test, terminate the instance
- Run
docker compose downso PostgreSQL shuts down cleanly. - Terminate the EC2 instance from the console. A plain stop proves nothing, because the instance’s disk still exists. Do not touch the EFS file system.
- Launch a new instance in the same VPC, with
sg-ec2and the SSM IAM role, in an Availability Zone that has an EFS mount target. - Connect with Session Manager and repeat the manual steps:
bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo -i
yum install -y amazon-efs-utils
mkdir -p /mnt/efs
mount -t efs -o tls fs-XXXXXXXX:/ /mnt/efs
yum install -y docker
systemctl enable --now docker
mkdir -p /usr/local/lib/docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
-o /usr/local/lib/docker/cli-plugins/docker-compose
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
cd /mnt/efs/app
docker compose up -d
docker compose exec db psql -U app -d appdb -c "SELECT * FROM test;"Notice that you do not recreate the compose file or the data folder. Both were already on EFS. If the
SELECT returns data stored on EFS, the recovery worked.A few details:
- PostgreSQL ignores
POSTGRES_PASSWORDwhen the data directory already contains a database, so the original password remains the real one. - The new instance has a different ID. Update
--targetin your SSM port-forwarding command.
(Insert a screenshot of the final
SELECT here.)What I learned
- An instance’s disk is disposable. Anything important must live outside it.
- Security groups come first when mounting fails: port 2049 from the instance to EFS is mandatory.
- Storing the
docker-compose.ymlon EFS makes recovery almost trivial: mount,cd,docker compose up -d. /etc/fstabprotects against reboots, not against terminated instances. Automation for new instances belongs in Terraform or user data.- You cannot unmount a file system while your shell is inside it.
- SSM removes the need for SSH keys and open ports, and port forwarding still lets you use tools like DBeaver.
Conclusions
For an early-stage startup, running the database in Docker with data on EFS is a cheap and fast starting point, and the data is already in an independent volume you can move later.
- Backups: use
pg_dumpregularly and keep the dumps in/mnt/efs/backups. - Migration to RDS: when you need automated backups, high availability, and less maintenance, restore your dump into Amazon RDS, or use AWS Database Migration Service for larger databases.
- Security improvements: keep your compose file in Git and move passwords to AWS Secrets Manager or SSM Parameter Store instead of plain text.
- Automation: put the install and mount commands in Terraform user data so every new instance mounts EFS on its own.
One honest caveat: EFS adds latency compared to a local disk, so for demanding production workloads RDS remains the better choice. EFS is an excellent stepping stone.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article