The ErrorYour CodeDeploy deployment dies mid-way with something like:
Script at specified location: scripts/before_install.sh failed to complete in 300 seconds
LifecycleEvent - BeforeInstall
Status - Failed
Deployment failed
The deployment gets marked failed in the CodeDeploy console, and your EC2 instance is left in a half-deployed state. This can happen in any lifecycle hook โ BeforeInstall, AfterInstall, ApplicationStart, ValidateService, you name it.
Root CauseCodeDeploy enforces a hard timeout of 300 seconds (5 minutes) per lifecycle hook script. Script doesn't exit in time? CodeDeploy kills it and fails the deployment.
Here's what typically causes scripts to hang or run long:
- Package manager (
apt,yum) waiting silently for a yes/no prompt or stuck on a slow mirror- Script hitting an internal URL or endpoint that's unreachable from the EC2 instance โ the TCP connection just hangs open-npm installwith hundreds of packages,pip installwith heavy ML dependencies, or similar installs that genuinely take several minutes- Script waiting for a systemd service to reachactivestate before continuing- Missingexit 0at the end โ a background process holds the shell open after the real work is done- An interactive prompt accidentally triggered inside the script (common withgpg,debconf, or certain installers)## Fix 1: Increase the Timeout in appspec.ymlThe fastest fix when your script legitimately needs more time. Each hook inappspec.ymlaccepts atimeoutkey in seconds, up to a maximum of 3600 (one hour):
hooks:
BeforeInstall:
- location: scripts/before_install.sh
timeout: 600
runas: root
ApplicationStart:
- location: scripts/start_server.sh
timeout: 900
runas: ec2-user
Redeploy after saving. CodeDeploy will now wait up to 10 or 15 minutes for those scripts to finish. When to use this: Dependency installs or database migrations that genuinely need more than 5 minutes. Don't bump the timeout as your first move without checking why the script is slow โ you may just be masking a hang.
Fix 2: Diagnose What the Script Is Actually DoingSSH into the EC2 instance and tail the CodeDeploy agent log while triggering a fresh deployment:
sudo tail -f /var/log/aws/codedeploy-agent/codedeploy-agent.log
Watch which line the output stops at. Still not clear? Add timestamps directly to your script to pinpoint the stall:
#!/bin/bash
set -e
echo "[$(date)] Starting before_install"
echo "[$(date)] Installing dependencies..."
apt-get install -y nginx
echo "[$(date)] Done"
The last timestamp you see in the log tells you exactly which command hung. You can also check the per-deployment script log:
/opt/codedeploy-agent/deployment-root/{deployment-group-id}/{deployment-id}/logs/scripts.log
Fix 3: Make Package Installs Non-InteractiveOne of the most common culprits: apt-get silently waiting for keyboard input, or pulling packages from a throttled mirror. Fix it by forcing non-interactive mode:
#!/bin/bash
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y --no-install-recommends nginx
On Amazon Linux or RHEL-based instances, yum is simpler โ the -y flag handles it:
yum install -y nginx
Never run apt-get install without -y inside a CodeDeploy script. The package manager will block forever waiting for a confirmation that never arrives.
Fix 4: Add Timeouts to Network CallsScripts that fetch config files, call internal APIs, or pull from S3 can hang indefinitely if a VPC route is missing or a security group is misconfigured. Always set explicit timeouts:
# Risky โ no timeout, hangs forever on a bad route
curl https://internal-config.example.com/config.json -o /etc/app/config.json
# Safe โ fails fast after 30 seconds, retries 3 times
curl --max-time 30 --retry 3 --retry-delay 5 \
https://internal-config.example.com/config.json \
-o /etc/app/config.json
Same idea with wget:
wget --timeout=30 --tries=3 https://internal-config.example.com/config.json
30 seconds per attempt with 3 retries means the worst case is 90 seconds โ well within the 300-second window.
Fix 5: Prevent Background Processes from Holding the Script OpenHere's a subtle one. Your script finishes its real work, starts a background service, but the shell stays open waiting for that service to report active. CodeDeploy times out even though everything actually worked.
#!/bin/bash
set -e
# This may block if the service takes a long time to become active
systemctl start myapp
# Better โ start it and keep moving
systemctl start myapp || true
# Or wait, but cap it at 60 seconds
timeout 60 bash -c 'until systemctl is-active myapp; do sleep 2; done'
Background jobs need to be fully detached too โ an ampersand alone isn't enough:
# Still attached to the shell
./start_worker.sh &
# Fully detached โ CodeDeploy won't wait for this
nohup ./start_worker.sh > /var/log/worker.log 2>&1 &
disown
Fix 6: Always End Scripts with exit 0Short fix, occasionally critical. On some shell configurations, a missing explicit exit leaves the process in limbo. CodeDeploy also treats any non-zero exit code as a failure. Close every script cleanly:
#!/bin/bash
set -e
# ... your script logic ...
exit 0
Verify the FixRedeploy and watch three places at once:
# On the EC2 instance โ real-time agent output
sudo tail -f /var/log/aws/codedeploy-agent/codedeploy-agent.log
# In the AWS Console
# CodeDeploy โ Deployments โ [your deployment] โ View events
# Per-deployment script log
ls /opt/codedeploy-agent/deployment-root/
# Navigate into the deployment folder โ logs/scripts.log
A clean run shows each lifecycle event with Status: Succeeded in the console. The script log will have complete output with no truncated lines at the end.

