AWS Builder Center

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)
DirectionProtocol / PortSource or destinationPurpose
InboundNonen/aNo SSH (22), no open ports
OutboundTCP 2049sg-efsTalk to EFS (NFS)
OutboundTCP 4430.0.0.0/0 or VPC endpointsSSM, yum, Docker image pulls
sg-efs (the EFS mount targets)
DirectionProtocol / PortSourcePurpose
InboundTCP 2049sg-ec2Only 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 AmazonSSMManagedInstanceCore policy.
  • 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-utils

Step 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/efs

Step 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 efs
Two important notes:
  • Leave the folder before unmounting. If your shell is inside /mnt/efs, umount fails with “device is busy”, and mount -a will just report that it is already mounted. Run cd ~ 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 version
Here 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.yml
yaml
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/data
Two 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.1 only. Nothing is exposed to the network, which fits our SSM-only design.
Start it:
bash
1
2
docker compose up -d
docker ps

Step 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 run session-manager-plugin to verify. On macOS: brew install --cask session-manager-plugin.
  • IAM permission ssm:StartSession for the instance and the AWS-StartPortForwardingSession document.
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 (or 5433)
  • 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.sql

Step 9: The real test, terminate the instance

  1. Run docker compose down so PostgreSQL shuts down cleanly.
  2. 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.
  3. Launch a new instance in the same VPC, with sg-ec2 and the SSM IAM role, in an Availability Zone that has an EFS mount target.
  4. 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_PASSWORD when the data directory already contains a database, so the original password remains the real one.
  • The new instance has a different ID. Update --target in 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.yml on EFS makes recovery almost trivial: mount, cd, docker compose up -d.
  • /etc/fstab protects 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_dump regularly 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article