OOM-killer instance rescue
August 30, 2025 · AI Conversations
[91735.272601] Out of memory: Killed process 778 (mysqld) total-vm:1802468kB, anon-rss:432908kB, file-rss:3072kB, shmem-rss:0kB, UID:111 pgtables:1292kB oom_score_adj:0
[96470.945974] Out of memory: Killed process 7952 (mysqld) total-vm:1832264kB, anon-rss:414272kB, file-rss:2944kB, shmem-rss:0kB, UID:111 pgtables:1244kB oom_score_adj:0
[96701.243189] Out of memory: Killed process 8569 (mysqld) total-vm:1599588kB, anon-rss:355224kB, file-rss:2688kB, shmem-rss:0kB, UID:111 pgtables:1064kB oom_score_adj:0
[183461.874978] Out of memory: Killed process 8648 (mysqld) total-vm:1791588kB, anon-rss:382692kB, file-rss:2944kB, shmem-rss:0kB, UID:111 pgtables:1176kB oom_score_adj:0
[262998.411441] Out of memory: Killed process 14980 (mysqld) total-vm:1807704kB, anon-rss:423024kB, file-rss:3200kB, shmem-rss:0kB, UID:111 pgtables:1264kB oom_score_adj:0
[263317.070604] Out of memory: Killed process 20944 (mysqld) total-vm:1838472kB, anon-rss:399084kB, file-rss:2816kB, shmem-rss:0kB, UID:111 pgtables:1216kB oom_score_adj:0
[263746.376052] Out of memory: Killed process 21153 (mysqld) total-vm:1562740kB, anon-rss:340508kB, file-rss:2816kB, shmem-rss:0kB, UID:111 pgtables:1024kB oom_score_adj:0
[263746.709878] systemd[1]: Failed to start mysql.service – MySQL Community Server.
[263963.934519] Out of memory: Killed process 21274 (mysqld) total-vm:1030256kB, anon-rss:332408kB, file-rss:2816kB, shmem-rss:0kB, UID:111 pgtables:964kB oom_score_adj:0
[276226.537839]
Instance reachability check failed
at aws instance
# A) Quick rescue (to get the box reachable again)
1) **If the instance is still unreachable**
– In EC2 console → select the instance → **Monitor and troubleshoot → Serial console** (or EC2 Instance Connect/SSM if enabled) and reboot once:
**Actions → Instance → Reboot.**
If it keeps failing, temporarily **Stop → change instance type to one with more RAM** (even just one size up), **Start** again.
2) **Once you have a shell, add swap (safety net)**
_On Ubuntu 22.04:_
# create 2G swap (bump to 4G if RAM is 1–2GB)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
# make swapping a bit more eager when memory is tight
echo 'vm.swappiness=20' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system
3) **Start MySQL and confirm it stays up**
sudo systemctl start mysql
sudo systemctl status mysql --no-pager
# if it fails, check logs:
sudo journalctl -u mysql -b --no-pager | tail -n 100
sudo tail -n 100 /var/log/mysql/error.log 2>/dev/null || true
# B) Right-size MySQL for your RAM
If you’re on a small instance (≤2–4 GB RAM), the default MySQL 8 config is too heavy. Create a lightweight override:
sudo mkdir -p /etc/mysql/conf.d
sudo nano /etc/mysql/conf.d/lowmem.cnf
Paste (choose ONE of these blocks):
**For 1–2 GB RAM**
[mysqld]
innodb_buffer_pool_size = 256M
innodb_log_file_size = 128M
innodb_buffer_pool_instances = 1
innodb_flush_method = O_DIRECT
max_connections = 40
table_open_cache = 1024
tmp_table_size = 32M
max_heap_table_size = 32M
performance_schema = OFF
**For 4 GB RAM**
[mysqld]
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_buffer_pool_instances = 1
innodb_flush_method = O_DIRECT
max_connections = 80
table_open_cache = 2048
tmp_table_size = 64M
max_heap_table_size = 64M
performance_schema = OFF
Then:
sudo systemctl restart mysql
# C) Trim other memory hogs (Apache/PHP)
– **PHP memory_limit** (common culprit in spikes):
# find your php.ini (example for PHP 8.3)
php -i | grep "Loaded Configuration File"
sudo nano /etc/php/8.3/apache2/php.ini
# set:
memory_limit = 128M # or 256M if needed
– **Restart Apache**:
sudo systemctl restart apache2
# D) Check what was eating RAM
# current usage
free -h
top -o %MEM # or htop if installed
# Apache processes & RSS
ps -e -o pid,comm,rss,cmd | awk '/apache2|httpd/ {print $0}' | sort -k3 -n | tail
# MySQL memory-related vars (once MySQL is up)
mysql -uroot -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW VARIABLES LIKE 'max_connections';"
# E) Add basic protection & monitoring
– Enable **systemd auto-restart** (usually default for MySQL), and consider lowering `MaxConnections` at app layer (Laravel DB pool / queue workers).
– Set up **CloudWatch Alarms** on `MemoryUtilization` (via CloudWatch Agent), and on `StatusCheckFailed_Instance` to be alerted before it locks up.
– If you have cron/queue workers: cap concurrency to what RAM allows.
# F) If it still OOMs
– Increase swap to **4 GB** (on small RAM boxes, swap is your airbag).
– Move to a slightly bigger instance (e.g., from t3.micro → t3.small/t3.medium; from t3a.small → t3a.medium/large).
– As a last resort if you can’t get in: **Stop instance → detach volume → attach to a rescue instance → edit `/etc/mysql/conf.d/lowmem.cnf` and `/etc/fstab` → reattach**.
—
If you tell me your instance type and approximate RAM, I’ll give you an exact `lowmem.cnf` and suggested `max_connections` for your load.
—
## 1. SSH into your EC2 instance
From your local machine (replace with your `.pem` and host):
ssh -i /path/to/key.pem ubuntu@your-ec2-public-dns
—
## 2. Log into MySQL
On Ubuntu, MySQL is usually configured with **auth_socket** for the `root` user, so the login command is:
sudo mysql
That will drop you into the MySQL shell as root without needing a password.
—
## 3. If you need a username/password login
If you created a user (e.g. `admin`) with a password:
mysql -u admin -p
It will then prompt you for the password.
—
## 4. If you want root to use a password instead of socket auth
Switch root’s plugin:
sudo mysql
Then inside MySQL:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';
FLUSH PRIVILEGES;
Now you can log in with:
mysql -u root -p
—
✅ **Tip:** If you just want to get in quickly on Ubuntu EC2, `sudo mysql` is the way.
—
Do you want me to also give you the steps to **reset the MySQL root password** in case you don’t know it?
# Option 1) Quickest (Ubuntu default): use socket auth
Many Ubuntu installs use `auth_socket` for MySQL root. Try:
# on the EC2 instance
sudo mysql
If this drops you into the MySQL prompt, set a password (and, if you want, switch to password auth):
Inside MySQL:
-- either keep socket auth and just create a separate admin user:
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'StrongPass#2025';
GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
-- or switch root to password auth:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'YourNewRootPass#2025';
-- (MySQL 8’s default plugin is fine too)
-- ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'YourNewRootPass#2025';
FLUSH PRIVILEGES;
Then you can log in with:
mysql -u root -p
# Option 2) If `sudo mysql` fails or you forgot the root password
Reset it safely using `–skip-grant-tables` temporarily:
# 1) stop mysql
sudo systemctl stop mysql
# 2) start mysqld without privilege checks (no network)
sudo mysqld_safe --skip-grant-tables --skip-networking &
# wait until you see "... ready for connections" in the output
Open a new shell tab/window and run:
mysql -u root
Inside MySQL, reset the password and plugin:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'YourNewRootPass#2025';
-- or:
-- ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'YourNewRootPass#2025';
Exit MySQL, then revert to normal startup:
# kill the temporary mysqld if needed
sudo killall mysqld_safe mysqld || true
# start mysql normally
sudo systemctl start mysql
sudo systemctl status mysql --no-pager
Test:
mysql -u root -p
# Option 3) Keep root on socket, use a dedicated app user (recommended)
If this EC2 hosts apps (Laravel/WordPress), it’s cleaner to leave `root` on `auth_socket` and create an app user with only the privileges it needs:
sudo mysql
Then:
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'AppStrong#2025';
GRANT ALL PRIVILEGES ON yourdbname.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
App connects with:
DB_HOST=127.0.0.1
DB_USER=appuser
DB_PASSWORD=AppStrong#2025
# Common gotchas
– **Service not running:**
`sudo systemctl status mysql` → if down: `sudo systemctl start mysql`
– **Wrong host:** If you created `user@'%'` or `user@'localhost'`, match it when connecting.
– **Firewall/Security Group:** Only matters for remote connections; for local `mysql -u …` it’s irrelevant.
– **Low RAM (your box OOM-killed mysqld earlier):** Add swap and trim MySQL memory (I can give you a low-RAM my.cnf if you tell me your instance RAM).
If you paste the exact error you get when `sudo mysql` or `mysql -u root -p` fails, I’ll pinpoint the precise fix.
Run this SQL:
SELECT user, host FROM mysql.user;
That will show every MySQL account and the host it’s allowed from (e.g. `root@localhost`, `admin@%`, etc.).
—
### Optional: see with authentication plugin too
SELECT user, host, plugin FROM mysql.user;
This is useful to check whether an account is using `auth_socket`, `mysql_native_password`, or `caching_sha2_password`.
—
### Example output
+------------------+-----------+-----------------------+
| user | host | plugin |
+------------------+-----------+-----------------------+
| root | localhost | auth_socket |
| mysql.session | localhost | mysql_native_password |
| mysql.sys | localhost | mysql_native_password |
| admin | localhost | caching_sha2_password |
+------------------+-----------+-----------------------+
—
⚡ Would you like me to also give you the **query to reset a user’s password** if you see one in the list but don’t know its password?
—
## 1. Connect to your EC2 instance
ssh -i /path/to/key.pem ubuntu@your-ec2-public-dns
—
## 2. Install PHP (if not already installed)
Adminer is just a PHP file. Install PHP + extensions (for MySQL):
sudo apt update
sudo apt install php php-mysql php-cli php-curl php-xml -y
If you’re running **Apache** (default with Ubuntu LAMP setups):
sudo apt install apache2 libapache2-mod-php -y
If you’re running **Nginx**, let me know — config is slightly different.
—
## 3. Download Adminer
Put it in a web-accessible folder (e.g. `/var/www/html`):
sudo mkdir -p /var/www/html/adminer
cd /var/www/html/adminer
sudo wget https://github.com/vrana/adminer/releases/download/v4.8.1/adminer-4.8.1.php -O index.php
—
## 4. Adjust permissions
sudo chown -R www-data:www-data /var/www/html/adminer
sudo chmod -R 755 /var/www/html/adminer
—
## 5. Access Adminer in browser
Now, in your browser go to:
http://<your-ec2-public-ip>/adminer/
You should see the Adminer login page 🎉
—
## 6. Secure it
⚠️ Important: never leave Adminer wide open on the public internet.
– **Option A:** Restrict with Apache config:
sudo nano /etc/apache2/sites-available/000-default.conf
Inside “ add:
<Directory /var/www/html/adminer>
Require ip YOUR.IP.ADDRESS
</Directory>
Then reload Apache:
sudo systemctl reload apache2
– **Option B:** Rename file (`index.php` → something secret like `dbtool.php`).
– **Option C (Best):** Put it behind Nginx reverse proxy with HTTP auth, or only allow via VPN/SSH tunnel.
—
✅ After this, log in with your MySQL user (`root` or `admin` that you created earlier).
—
Do you want me to also give you the **steps to password-protect Adminer with Apache/Nginx** so it isn’t exposed to the whole internet?
—
## 1. Verify Apache is running
SSH into your EC2:
sudo systemctl status apache2 --no-pager
You should see it as **active (running)**.
If not, start it:
sudo systemctl start apache2
sudo systemctl enable apache2
—
## 2. Test locally on the server
Run:
curl http://localhost
– If you see the **Apache2 Ubuntu Default Page**, Apache is working fine locally.
– If you don’t, Apache config or PHP is broken — let me know what you see.
—
## 3. Check Security Group (AWS firewall)
In the AWS EC2 console → your instance → **Security Groups**:
– Make sure **Inbound rules** include:
– **HTTP (port 80)** from `0.0.0.0/0`
– **HTTPS (port 443)** from `0.0.0.0/0` (optional if you’ll use SSL)
– **SSH (port 22)** from your IP
If HTTP/80 isn’t there, add it and save.
—
## 4. Check Ubuntu firewall (ufw/iptables)
If you enabled `ufw`:
sudo ufw status
If it’s active, allow Apache:
sudo ufw allow 'Apache Full'
—
## 5. Access via Public IPv4
In your browser, open:
http://<your-ec2-public-ip>/adminer/
⚠️ Do **not** use the `ip-172-31-…` private DNS — that only works inside AWS VPC. Use the **Public IPv4 address** or the **Public DNS** listed in your EC2 console.
—
## 6. (Optional) Check Apache site root
Make sure Adminer is really in the web root:
ls -l /var/www/html/adminer
You should see `index.php`. Then you can hit:
http://<your-ec2-public-ip>/adminer/
—
### 🔑 Quick checklist
– Apache is running ✅
– `curl localhost` works ✅
– Security group allows port 80 ✅
– Using **public IP/DNS**, not private ✅
– Adminer is in `/var/www/html/adminer/index.php` ✅
—
👉 Can you try `curl http://localhost` on your EC2 and tell me what it prints? That will confirm if it’s an Apache issue or just AWS firewall/networking.
● apache2.service – The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
Active: active (running) since Sat 2025-08-30 05:35:46 UTC; 5min ago
Docs: https://httpd.apache.org/docs/2.4/
Process: 27163 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS)
Main PID: 27166 (apache2)
Tasks: 6 (limit: 1008)
Memory: 11.3M (peak: 11.6M)
CPU: 74ms
CGroup: /system.slice/apache2.service
├─27166 /usr/sbin/apache2 -k start
├─27168 /usr/sbin/apache2 -k start
├─27169 /usr/sbin/apache2 -k start
├─27170 /usr/sbin/apache2 -k start
├─27171 /usr/sbin/apache2 -k start
└─27172 /usr/sbin/apache2 -k start
## 1) Check from inside the server (confirms Apache works)
# should print the Apache default HTML
curl -I http://localhost
You want `HTTP/1.1 200 OK`. If that works, Apache is fine locally.
Also confirm Adminer file exists:
ls -l /var/www/html/adminer
# expect: index.php present
## 2) Is Apache listening on 0.0.0.0:80?
sudo ss -tlnp | grep ':80'
# expect something like: LISTEN 0 511 0.0.0.0:80 ... users:(("apache2",pid=...,fd=...))
## 3) Ubuntu firewall (ufw) isn’t blocking?
sudo ufw status
# if "active", allow Apache:
sudo ufw allow 'Apache Full'
## 4) Use the **Public IPv4** (not the private ip-172-31-…)
That `ip-172-31-12-79` hostname is **private** (VPC only). From the server, get your public IP:
curl -s http://169.254.169.254/latest/meta-data/public-ipv4
In your browser open:
http://<that-public-ip>/adminer/
(If empty output, your instance likely has **no public IPv4** attached.)
## 5) Security Group must allow HTTP
In EC2 console → Instance → **Security** tab → Security groups → Inbound rules:
– Add **HTTP** TCP **port 80** from `0.0.0.0/0`
– (Optional) **HTTPS** TCP **443** from `0.0.0.0/0`
– Keep **SSH 22** from your IP.
If you use IPv6, also allow **HTTP (80)** from `::/0`.
## 6) If you still can’t reach it from the internet
– **No public IP?** Attach one:
– EC2 → **Elastic IPs** → Allocate → Associate to your instance (primary network interface).
– **Subnet routing:** VPC → **Route tables** → ensure your subnet has a route to an **Internet Gateway (igw-… )** for `0.0.0.0/0` (and `::/0` if IPv6).
– **Network ACLs:** VPC → **Network ACLs** for the subnet should allow inbound/outbound **80/1024-65535** (stateless rules).
– **Provider/ISP blocking 80?** Try from another network or use `http://PUBLIC-IP/` via `curl` from an external host:
curl -I http://PUBLIC.IP.ADDR
## 7) (Optional) Verify Apache site root and vhost
Default docroot is `/var/www/html`. Confirm your site config:
apache2ctl -S
# and:
grep -R "DocumentRoot" /etc/apache2/sites-enabled/
For Adminer you should have `/var/www/html/adminer/index.php`.
—
### Quick sanity checklist
– `curl -I http://localhost` → 200 OK ✅
– `ss -tlnp` shows Apache on `0.0.0.0:80` ✅
– `ufw` allows Apache or is inactive ✅
– You’re using the **Public IPv4**/**Public DNS**, not `ip-172-31-*` ✅
– Security Group allows inbound **80** from anywhere ✅
– Instance/subnet has public IP + route to Internet Gateway ✅
If you run steps 1–4 and tell me the outputs (especially the public IP line and `curl -I http://localhost`), I’ll pinpoint exactly what’s missing.
—
## 1. SSH into your EC2 instance
ssh -i /path/to/key.pem ubuntu@your-ec2-public-ip
—
## 2. Install Git
sudo apt update
sudo apt install git -y
Check:
git --version
—
## 3. Configure Git identity (one-time)
git config --global user.name "Your Name"
git config --global user.email "youremail@example.com"
—
## 4. Generate SSH key (best practice for GitHub)
On your EC2 instance:
ssh-keygen -t ed25519 -C "youremail@example.com"
– Press **Enter** for default path (`/home/ubuntu/.ssh/id_ed25519`)
– Passphrase optional (can leave blank for automation)
Start SSH agent & add key:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
—
## 5. Add the public key to GitHub
Copy the key:
cat ~/.ssh/id_ed25519.pub
Go to **GitHub → Settings → SSH and GPG keys → New SSH key**
Paste it in and save.
—
## 6. Test connection
ssh -T git@github.com
You should see:
Hi username! You've successfully authenticated, but GitHub does not provide shell access.
—
## 7. Clone your repo
Choose the SSH URL from GitHub (not HTTPS). Example:
cd /var/www/html
sudo git clone git@github.com:your-username/your-repo.git
—
## 8. Deploy workflow
– Pull updates:
cd /var/www/html/your-repo
git pull origin main
– If using Apache/PHP: make sure your repo files are inside `/var/www/html` (or update Apache `DocumentRoot`).
– Fix permissions if needed:
sudo chown -R www-data:www-data /var/www/html/your-repo
sudo chmod -R 755 /var/www/html/your-repo
—
✅ At this point, your EC2 is linked to GitHub. You can deploy just by `git pull`.
—
👉 Do you want me to also show you how to set up **automatic deployment** (so whenever you push to GitHub, it auto-updates your EC2 via webhook or GitHub Actions)?
Cloning into 'myapicalls'…
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
Do this step-by-step:
## 1) Make sure the repo URL & access are correct
# replace with your repo if different
REPO=git@github.com:NeerajTi/myapicalls.git
If it’s private, your GitHub account must have access.
## 2) Create (or reuse) an SSH key for the **ubuntu** user
# as ubuntu (no sudo)
ls -la ~/.ssh
# if no id_ed25519.pub exists, create one:
ssh-keygen -t ed25519 -C "your-email@example.com"
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
Copy the output, then in GitHub → **Settings → SSH and GPG keys → New SSH key** → paste → Save.
(If your org requires SSO, click **Enable SSO** on that key.)
Test:
ssh -T git@github.com
# expect: "Hi <username>! You've successfully authenticated..."
## 3) Don’t use sudo for the clone
Either make `/var/www/html` writable by `ubuntu` or clone in your home and move:
### Option A (recommended): grant ubuntu write to web root
sudo chown -R ubuntu:www-data /var/www/html
sudo chmod -R 775 /var/www/html
# now clone WITHOUT sudo
cd /var/www/html
git clone git@github.com:NeerajTi/myapicalls.git
### Option B: clone in home, then move with sudo
cd ~
git clone git@github.com:NeerajTi/myapicalls.git
sudo mv ~/myapicalls /var/www/html/
## 4) If it still says “Permission denied (publickey)”
Quick diagnostics:
# verbose SSH to see which key is offered
GIT_SSH_COMMAND="ssh -v" git ls-remote git@github.com:NeerajTi/myapicalls.git
Common fixes:
– Ensure the key file permissions are tight:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
– Make sure you’re **not** using `sudo` for the git command.
– Confirm the repo path and capitalization are exact.
– If you must use root (not advised), create & add a separate key for root at `/root/.ssh/` and add that public key to GitHub too.
## 5) Alternative (HTTPS)
If it’s public:
git clone https://github.com/NeerajTi/myapicalls.git
If private, use a **Personal Access Token** when prompted for password (GitHub passwords don’t work).
—
Run those steps and try the clone again without `sudo`. If it errors, paste the output of the verbose check (`ssh -v …`) and I’ll pinpoint the exact issue.
—
## 1. Create a GitHub Token
1. Go to **GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)**
or **Settings → Developer settings → Fine-grained tokens** (new way).
2. Generate a new token with at least:
– `repo` scope (full repo access).
3. Copy the token (looks like `ghp_xxxxx…`). Keep it safe.
—
## 2. Use HTTPS URL for clone
Instead of SSH:
git clone https://github.com/NeerajTi/myapicalls.git
When prompted:
– **Username:** your GitHub username (`NeerajTi`)
– **Password:** paste your **token** (not your GitHub password).
—
## 3. Store credentials (optional)
If you don’t want to type the token every time:
git config --global credential.helper store
Then do the clone/pull once, enter user + token, and Git will remember it in `~/.git-credentials`.
—
## 4. Example full flow
cd /var/www/html
git clone https://github.com/NeerajTi/myapicalls.git
Prompt:
Username for 'https://github.com': NeerajTi
Password for 'https://NeerajTi@github.com': <paste token here>
—
✅ After this, you’ll have `/var/www/html/myapicalls` with your repo.
—
Do you want me to also show you how to **set up automatic pulls (deploy on push)** using GitHub Actions + your token, so EC2 updates itself whenever you push code?
### 1) Add these GitHub Secrets (Repo → Settings → Secrets and variables → Actions → New repository secret)
– `EC2_HOST` → your EC2 public IP or public DNS (e.g. `13.234.xx.xx` or `ec2-xx-xx-xx.compute.amazonaws.com`)
– `EC2_USER` → usually `ubuntu`
– `EC2_PATH` → remote folder to receive XMLs, e.g. `/var/www/html/myapicalls/xml`
– `EC2_SSH_KEY` → the **private** SSH key that can SSH into the instance (contents of your `~/.ssh/id_ed25519` or `.pem` converted to OpenSSH)
> Make sure the key’s corresponding public key is already in `~/.ssh/authorized_keys` on the EC2 instance.
### 2) Ensure the remote directory exists (one-time on EC2)
ssh -i /path/to/key ubuntu@EC2_HOST "sudo mkdir -p /var/www/html/myapicalls/xml && sudo chown -R ubuntu:www-data /var/www/html/myapicalls && sudo chmod -R 775 /var/www/html/myapicalls"
### 3) Create the workflow in your repo
Save this as **`.github/workflows/deploy-xml-to-ec2.yml`**:
name: Deploy XML to EC2
on:
push:
branches: [ "main" ]
paths:
- "**/*.xml" # only run when XML files change
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Make sure remote path exists
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
mkdir -p ${{ secrets.EC2_PATH }}
- name: Copy XML files to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
source: "**/*.xml"
target: ${{ secrets.EC2_PATH }}
overwrite: true
strip_components: 0
rm: false
# Optional: fix ownership/permissions or reload services
- name: Post-deploy permissions & touch
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
sudo chown -R www-data:www-data ${{ secrets.EC2_PATH }}
sudo find ${{ secrets.EC2_PATH }} -type f -name "*.xml" -exec chmod 664 {} ;
sudo find ${{ secrets.EC2_PATH }} -type d -exec chmod 775 {} ;
# If your app needs a cache clear or reload, do it here:
# sudo systemctl reload apache2
### How it works
– Triggers **only when XML files change** on `main`.
– Creates the target folder if missing.
– Securely copies changed **`.xml`** files to your EC2 folder.
– (Optional) Fixes ownership/permissions and can reload Apache if you want.
### Want to deploy the whole repo or a specific folder?
– Replace `source: "**/*.xml"` with:
– a folder (e.g., `source: "public/xml/**"`), or
– the whole repo (e.g., `source: "**/*"`), plus a `paths: ["**"]` trigger.
### Common pitfalls to avoid
– Using the **private** key content (not the `.pub`) in `EC2_SSH_KEY`.
– EC2 Security Group must allow **SSH (22)** from GitHub runners or from anywhere (0.0.0.0/0) if you’re okay with that temporarily.
– If strict host key checking bites you, appleboy actions disable it by default; otherwise, you can pre-add known_hosts with `ssh-keyscan`.
If you tell me your exact target folder (where your site/app reads the XMLs), I can tweak the workflow to match it perfectly (and, if needed, add a quick Apache/Nginx reload step).
# Ensure that the correct permissions are set on the EC2 instance
ssh -i key.pem -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST "sudo chown -R $EC2_USER:$EC2_USER $EC2_PATH && sudo chmod -R 775 $EC2_PATH"
shell: /usr/bin/bash -e {0}
env:
EC2_USER: ***
EC2_HOST: ***
EC2_PATH: ***
Warning: Permanently added '***' (ED25519) to the list of known hosts.
***@***: Permission denied (publickey).
# Step 1 — Create a dedicated deploy key pair
On **your machine** (or anywhere secure):
ssh-keygen -t ed25519 -C "github-actions-ec2" -f ./ec2_deploy_key
# creates ec2_deploy_key (private) and ec2_deploy_key.pub (public)
# Step 2 — Install the **public** key on EC2
SSH to EC2 using your existing AWS key (the one that works):
ssh -i /path/to/aws-key.pem ubuntu@<EC2_PUBLIC_IP>
Then:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# paste the entire contents of ec2_deploy_key.pub below:
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI........ github-actions-ec2" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chown -R ubuntu:ubuntu ~/.ssh
# Step 3 — Save the **private** key in GitHub Secrets
In your repo: **Settings → Secrets and variables → Actions → New repository secret**
– `EC2_HOST` → your public IP / DNS
– `EC2_USER` → `ubuntu`
– `EC2_PATH` → e.g. `/var/www/html/myapicalls/xml`
– `EC2_SSH_KEY` → paste **entire contents** of `ec2_deploy_key` (the private key)
> Don’t paste the `.pub` file here. Also make sure there are **no extra spaces** or Windows line endings.
# Step 4 — Open SSH (22) in the Security Group
EC2 → Instance → **Security** tab → Inbound rules → allow **TCP 22** from `0.0.0.0/0` (or your office IP range).
(You can lock it down later.)
# Step 5 — Use a hardened step in your workflow
Replace your failing “chmod/chown” step with this tested block (works for any SSH command):
- name: SSH - fix ownership/permissions on EC2
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ec2_deploy_key
chmod 600 ~/.ssh/ec2_deploy_key
# Optionally pin host key to avoid MITM warnings (keeps StrictHostKeyChecking on)
ssh-keyscan -H "${{ secrets.EC2_HOST }}" >> ~/.ssh/known_hosts
ssh -i ~/.ssh/ec2_deploy_key -o StrictHostKeyChecking=yes
"${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}"
"sudo chown -R ${{ secrets.EC2_USER }}:${{ secrets.EC2_USER }} '${{ secrets.EC2_PATH }}' && sudo chmod -R 775 '${{ secrets.EC2_PATH }}'"
Or use the Appleboy action (simpler):
- name: SSH - fix ownership/permissions
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
sudo chown -R ${{ secrets.EC2_USER }}:${{ secrets.EC2_USER }} "${{ secrets.EC2_PATH }}"
sudo chmod -R 775 "${{ secrets.EC2_PATH }}"
# Quick diagnostics if it still says `Permission denied (publickey)`
Run these from the workflow (or locally to test):
# From workflow: verbose SSH (great error details)
GIT_SSH_COMMAND="ssh -vvv -i ~/.ssh/ec2_deploy_key"
ssh -vvv -i ~/.ssh/ec2_deploy_key -o StrictHostKeyChecking=no
${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} 'echo ok'
On EC2, double-check:
# correct user?
whoami # should be ubuntu
# the public key really installed?
cat ~/.ssh/authorized_keys
# perms are strict
ls -ld ~/.ssh && ls -l ~/.ssh
# expect: .ssh = 700, authorized_keys = 600, owner ubuntu:ubuntu
**Common causes**
– You pasted the **.pub** key into `EC2_SSH_KEY` (should be the **private** key).
– You added the public key to the wrong user’s `authorized_keys` (must be `/home/ubuntu/.ssh/authorized_keys` if you SSH as `ubuntu`).
– Wrong user (Amazon Linux uses `ec2-user`; Ubuntu uses `ubuntu`).
– Security Group blocks port 22.
– The instance has no public IPv4 or you’re using the private 172.31.* address from outside the VPC.
Do the 5 steps above and your deploy step will connect cleanly. If you want, paste your current workflow (without secrets) and I’ll slot in the exact fixes.
Here’s how to do it on **Ubuntu + Apache2**:
—
## 1. Check your current DocumentRoot
Default Apache config is at:
cat /etc/apache2/sites-available/000-default.conf
You’ll see something like:
<VirtualHost *:80>
DocumentRoot /var/www/html
...
</VirtualHost>
—
## 2. Change DocumentRoot to `/var/www/html/myapicalls`
Edit:
sudo nano /etc/apache2/sites-available/000-default.conf
Update:
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/myapicalls
<Directory /var/www/html/myapicalls>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
—
## 3. Enable `.htaccess` (if you’re using Laravel, WordPress, etc.)
Make sure `mod_rewrite` is enabled:
sudo a2enmod rewrite
Restart Apache:
sudo systemctl restart apache2
—
## 4. Verify permissions
sudo chown -R www-data:www-data /var/www/html/myapicalls
sudo chmod -R 755 /var/www/html/myapicalls
—
## 5. Test
Open your browser:
http://<your-ec2-public-ip>/
It should now load the app from `/myapicalls`.
—
✅ Optional: If you also want to serve via a **domain name** (not just IP), point your domain’s **A record** to the EC2 public IP, then update the Apache config:
ServerName yourdomain.com
ServerAlias www.yourdomain.com
—
👉 Do you want me to also set it up so **`mydomain.com` auto-redirects to `https://www.mydomain.com`** with SSL (Let’s Encrypt)?
Fix it with an **Alias** so `/adminer/` maps to the old path:
1) Edit your vhost:
sudo nano /etc/apache2/sites-available/000-default.conf
2) Keep your current DocumentRoot, then add this block **inside** “:
Alias /adminer /var/www/html/adminer
<Directory /var/www/html/adminer>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
3) Reload Apache:
sudo systemctl reload apache2
4) Test:
curl -I http://localhost/adminer/
You should get `200 OK`. Now open `http:///adminer/`.
—
### (Optional) Lock Adminer down (recommended)
Replace `Require all granted` with your IP:
<Directory /var/www/html/adminer>
Require ip YOUR.PUBLIC.IP.ADDR
</Directory>
Or add HTTP auth; I can give you a quick snippet if you want.
—
### Alternative quick hack (not as clean)
Symlink Adminer into your new docroot:
sudo ln -s /var/www/html/adminer /var/www/html/myapicalls/adminer
sudo systemctl reload apache2
Alias is cleaner.
<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName YOUR_DOMAIN_OR_PUBLIC_IP
DocumentRoot /var/www/html/myapicalls
DirectoryIndex index.php index.html
<Directory /var/www/html/myapicalls>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Keep Adminer reachable at /adminer
Alias /adminer /var/www/html/adminer
<Directory /var/www/html/adminer>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
After saving:
sudo a2enmod rewrite
sudo systemctl reload apache2
Now:
– Root: `http:///` → serves `/var/www/html/myapicalls`
– Adminer: `http:///adminer/` → serves `/var/www/html/adminer`
If you want me to prep the SSL vhost (`default-ssl.conf`) too, say the word and I’ll give the full file.
## 1) See how Apache runs PHP
# Does Apache use mod_php or PHP-FPM?
apachectl -M | grep -E 'php|proxy_fcgi' || true
# What PHP version is installed?
php -v
dpkg -l | grep -E '^iis+php' | cut -c1-80
## 2) Install MySQL extensions (pick the right version)
On Ubuntu, the meta-package `php-mysql` installs **mysqli** + **pdo_mysql** (mysqlnd). If you’re on PHP 8.3, you can use the versioned package too.
### Common, works for most:
sudo apt update
sudo apt install -y php-mysql
### If you know you’re on PHP 8.3 (example):
sudo apt update
sudo apt install -y php8.3-mysql
## 3) Restart the right service(s)
– If Apache uses **mod_php**:
sudo systemctl restart apache2
– If Apache uses **PHP-FPM** (you saw `proxy_fcgi` above):
# adjust version if not 8.3
sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2
## 4) Quick verify
php -m | grep -i -E 'mysql|pdo'
# Expect: mysqli, pdo_mysql
(Optional) Create a phpinfo page to confirm from the web side:
echo '<?php phpinfo();' | sudo tee /var/www/html/myapicalls/info.php
Open: `http:///info.php` → look for `mysqli` and `pdo_mysql` under “Additional .ini files parsed”.
## 5) If it still shows “not available”
– You installed the extension for a **different PHP version** than Apache uses. Install the **matching** package (`phpX.Y-mysql`) for that version.
– If using **mod_php** and **php-fpm** both by accident, disable one to avoid confusion:
# disable mod_php (if using FPM)
sudo a2dismod php8.3
sudo systemctl restart apache2
# OR disable FPM integration (if using mod_php)
sudo a2disconf php8.3-fpm
sudo systemctl restart apache2
– Ensure no conflicting `open_basedir`/`disable_functions` settings are blocking extensions.
Once `mysqli`/`pdo_mysql` show up, Adminer will let you log in. If you paste the outputs of steps 1 and 4, I’ll tailor exact commands for your version.
—
# 🔹 Step 1: Point your domain to EC2
1. In your domain registrar’s DNS panel (GoDaddy, Namecheap, Cloudflare, etc.):
– Create an **A record**:
Name: @
Type: A
Value: <Your EC2 Public IPv4 address>
TTL: 300 (or lowest allowed)
– (Optional) Add **www**:
Name: www
Type: CNAME
Value: yourdomain.com
TTL: 300
2. Wait up to a few minutes (sometimes hours) for propagation.
You can check with:
dig +short yourdomain.com
—
# 🔹 Step 2: Configure Apache VirtualHost for your domain
Edit config:
sudo nano /etc/apache2/sites-available/yourdomain.conf
Paste:
<VirtualHost *:80>
ServerName yourdomain.com
ServerAlias www.yourdomain.com
DocumentRoot /var/www/html/myapicalls
<Directory /var/www/html/myapicalls>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Keep Adminer accessible
Alias /adminer /var/www/html/adminer
<Directory /var/www/html/adminer>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/yourdomain_error.log
CustomLog ${APACHE_LOG_DIR}/yourdomain_access.log combined
</VirtualHost>
Enable site + reload:
sudo a2ensite yourdomain.conf
sudo a2dissite 000-default.conf # disable default
sudo systemctl reload apache2
Now `http://yourdomain.com` should serve your app.
—
# 🔹 Step 3: Install Certbot (Let’s Encrypt SSL)
sudo apt update
sudo apt install certbot python3-certbot-apache -y
Run Certbot:
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
– It will detect Apache vhost, install SSL, and update config.
– Choose option `2` (redirect all traffic to HTTPS).
—
# 🔹 Step 4: Auto-renewal
Let’s Encrypt certs last 90 days. Cron job is auto-installed. Test with:
sudo certbot renew --dry-run
—
# 🔹 Step 5: Verify
Open:
https://yourdomain.com
✅ Should show a padlock.
—
👉 Do you want me to also include a redirect so `http://yourdomain.com/adminer` only works **over HTTPS** (and maybe restricted by IP), so it’s not exposed to the whole internet?
Here’s the clean way:
—
## 1. Move PEM file to `~/.ssh`
Let’s say your key is `key.pem`. Move and secure it:
mkdir -p ~/.ssh
mv ~/Downloads/key.pem ~/.ssh/
chmod 400 ~/.ssh/key.pem
—
## 2. Find your EC2 Public DNS / IP
Go to AWS Console → **EC2 → Instances → your instance** → copy either:
– **Public IPv4 DNS** → e.g.
`ec2-16-170-254-93.eu-north-1.compute.amazonaws.com`
– or **Public IPv4 address** → e.g. `16.170.254.93`
—
## 3. SSH into EC2
On Ubuntu terminal:
ssh -i ~/.ssh/key.pem ubuntu@ec2-16-170-254-93.eu-north-1.compute.amazonaws.com
⚡ Notes:
– Use `ubuntu` (default user for Ubuntu AMIs).
– For Amazon Linux use `ec2-user`.
– For Debian use `admin`.
—
## 4. First-time host key warning
You’ll see:
The authenticity of host 'ec2-xxx' can't be established.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Type:
yes
—
## 5. Done 🎉
You’ll now be inside your EC2 shell as `ubuntu`.
—
👉 Do you also want me to show you how to set up **VS Code Remote SSH** with this `.pem` file so you can edit your EC2 files directly in VS Code?