Unable to start service apache2: Job for apache2.service failed because the control process exited with error code - the three real causes and the Ansible playbook that shows you which one (reproduced on Ubuntu 24.04)

TL;DR

  • The message is a relay, not a reason. Ansible's service module ran systemctl start apache2, systemd ran apachectl start, apachectl exited non-zero. The reason is in journalctl -u apache2 -n 20 on the target.
  • Three causes cover almost every case: something else on port 80 (AH00072: make_sock: could not bind to address 0.0.0.0:80), a syntax error in a config file (AH00526: Syntax error on line 1 of ...: Invalid command 'Lisen'), and a directive from a module that is not enabled (Invalid command 'SSLEngine', fix with a2enmod ssl).
  • apache2ctl configtest finds the second and third in 18 ms without touching the running service. Run it as a task before service: state=started.
  • The playbook pattern: a block with configtest then service, and a rescue that prints the last 15 journal lines, so the failure message tells you what happened instead of sending you to the server.
  • Every output below is real, from ansible-core 2.21.5 against an Ubuntu 24.04 container with Apache 2.4.58 on 2026-10-16.
  • Not your exact case? The closest post on this site is Ansible Handlers Explained.

The 2023 version of this post listed things to check. This time I broke Apache three ways on purpose, ran the same playbook against each, and kept the output, so you can match your journal lines to one of the three.

Table of Content

  1. What the error actually says
  2. The playbook that fails, and the one that diagnoses
  3. Cause 1 - port 80 is already taken
  4. Cause 2 - a typo in a config file
  5. Cause 3 - a directive from a module that is not enabled
  6. The fix in the playbook - validate, start, show the journal on failure
  7. What broke when I ran this
  8. Conclusion

1. What the error actually says

1fatal: [web]: FAILED! => {"changed": false, "msg": "Unable to start service apache2: Job for apache2.service failed because the control process exited with error code.\nSee \"systemctl status apache2.service\" and \"journalctl -xeu apache2.service\" for details.\n"}

Three layers produced that line, and only the bottom one knows why:

LayerWhat it didWhat it tells you
Ansible ansible.builtin.serviceran systemctl start apache2, got exit code 1"Unable to start service apache2"
systemdran the unit's ExecStart=apachectl start, the control process exited 1"Job for apache2.service failed because the control process exited with error code"
apachectl / apache2tried to parse the config and bind the portsthe real reason, in the journal: AH00072, AH00526, ...

So the first move is always the same, on the target or through Ansible: journalctl -u apache2 --no-pager -n 20. The lines that start with apachectl[...] are Apache's own; the AH codes are indexed in the Apache documentation and a search for the code finds the cause faster than the message text.

The error as Ansible prints it: the reason is not in this output

From the journal line to the cause: the three AH codes behind almost every apache2 start failure


2. The playbook that fails, and the one that diagnoses

The playbook everyone has:

1# start.yml
2- hosts: web
3  tasks:
4    - name: Ensure Apache is running
5      ansible.builtin.service:
6        name: apache2
7        state: started

And a second one I keep next to it, which runs the three checks that find all three causes:

 1# diagnose.yml
 2- hosts: web
 3  tasks:
 4    - name: Syntax check
 5      ansible.builtin.command: apache2ctl configtest
 6      register: ct
 7      ignore_errors: true
 8    - debug: var=ct.stderr_lines
 9
10    - name: Last journal lines
11      ansible.builtin.command: journalctl -u apache2 --no-pager -n 8
12      register: jl
13    - debug: var=jl.stdout_lines
14
15    - name: What is on port 80
16      ansible.builtin.shell: ss -ltnp | grep ':80 ' || true
17      register: ss
18    - debug: var=ss.stdout_lines

On a healthy target the first playbook does what it should (ansible-core 2.21.5, Apache 2.4.58):

1$ ansible-playbook -i inventory.ini start.yml
2
3TASK [Ensure Apache is running] ************************************************
4changed: [web]
5
6PLAY RECAP *********************************************************************
7web                        : ok=2    changed=1    unreachable=0    failed=0

The next three sections break the target and run both playbooks. The service module output is identical in all three cases, which is the whole point: you cannot tell them apart from Ansible's message.


3. Cause 1 - port 80 is already taken

I stopped Apache and started nginx, the most common real-world version of this (a second web server, a stale Apache process, a container publishing port 80):

 1$ ansible-playbook -i inventory.ini start.yml
 2
 3TASK [Ensure Apache is running] ************************************************
 4fatal: [web]: FAILED! => {"changed": false, "msg": "Unable to start service apache2: Job for apache2.service failed because the control process exited with error code.\nSee \"systemctl status apache2.service\" and \"journalctl -xeu apache2.service\" for details.\n"}
 5
 6$ ansible-playbook -i inventory.ini diagnose.yml
 7
 8TASK [debug] *******************************************************************
 9    "ct.stderr_lines": [
10        "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message",
11        "Syntax OK"
12    ]
13    "jl.stdout_lines": [
14        "systemd[1]: Starting apache2.service - The Apache HTTP Server...",
15        "apachectl[1459]: (98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80",
16        "apachectl[1459]: no listening sockets available, shutting down",
17        "apachectl[1459]: AH00015: Unable to open logs",
18        "systemd[1]: apache2.service: Control process exited, code=exited, status=1/FAILURE",
19        "systemd[1]: Failed to start apache2.service - The Apache HTTP Server."
20    ]
21    "ss.stdout_lines": [
22        "LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:((\"nginx\",pid=1329,fd=5),(\"nginx\",pid=1328,fd=5),(\"nginx\",pid=1327,fd=5))",
23        "LISTEN 0      511             [::]:80           [::]:*    users:((\"nginx\",pid=1329,fd=6),(\"nginx\",pid=1328,fd=6),(\"nginx\",pid=1327,fd=6))"
24    ]

Syntax OK plus AH00072: make_sock: could not bind to address 0.0.0.0:80 is this cause, and ss -ltnp names the owner: nginx, PIDs 1327 to 1329. AH00015: Unable to open logs below it is a consequence, not a second problem. The fix is one of: stop and disable the other service (systemctl disable --now nginx), or change Apache's port in /etc/apache2/ports.conf (the Listen directive). The AH00558 line about ServerName is a warning that appears on every run and is not the cause; set ServerName localhost in apache2.conf to silence it. ss is the modern replacement for the netstat I used in 2023 and is documented in ss(8).

The same check with netstat on the 2023 server: another process owns port 80


4. Cause 2 - a typo in a config file

One misspelled directive in any enabled file (Lisen 8080 in /etc/apache2/conf-enabled/typo.conf):

 1$ ansible-playbook -i inventory.ini diagnose.yml
 2
 3TASK [Syntax check] ************************************************************
 4fatal: [web]: FAILED! => {"changed": true, "cmd": ["apache2ctl", "configtest"], "delta": "0:00:00.018219", "rc": 1, "stderr_lines": ["AH00526: Syntax error on line 1 of /etc/apache2/conf-enabled/typo.conf:", "Invalid command 'Lisen', perhaps misspelled or defined by a module not included in the server configuration"]}
 5...ignoring
 6
 7    "jl.stdout_lines": [
 8        "systemd[1]: Starting apache2.service - The Apache HTTP Server...",
 9        "apachectl[1969]: AH00526: Syntax error on line 1 of /etc/apache2/conf-enabled/typo.conf:",
10        "apachectl[1969]: Invalid command 'Lisen', perhaps misspelled or defined by a module not included in the server configuration",
11        "systemd[1]: apache2.service: Control process exited, code=exited, status=1/FAILURE"
12    ]
13    "ss.stdout_lines": []

AH00526 gives the file and the line number; the journal and configtest say exactly the same thing, but configtest said it in 18 ms (delta 0:00:00.018219) without a start attempt. Fix the line, run apache2ctl configtest until it prints Syntax OK, then start. If the file was written by an Ansible template task, the typo is in your template; add validate: 'apache2ctl -t -f %s' to that task so the typo fails the template step rather than the service step.

apache2ctl configtest printing Syntax OK after the fix


5. Cause 3 - a directive from a module that is not enabled

The same AH00526, but the directive is spelled correctly. A virtual host with SSLEngine on on a server where mod_ssl is not enabled:

1    "ct.stderr_lines": [
2        "AH00526: Syntax error on line 2 of /etc/apache2/sites-enabled/bad.conf:",
3        "Invalid command 'SSLEngine', perhaps misspelled or defined by a module not included in the server configuration"
4    ]

The second half of the message, defined by a module not included in the server configuration, is the clue. SSLEngine needs mod_ssl, ProxyPass needs mod_proxy, RewriteEngine needs mod_rewrite, Header needs mod_headers. The fix is a2enmod ssl (or the module in question) before the vhost is enabled, and in Ansible that is a task with community.general.apache2_module or simply command: a2enmod ssl with creates: /etc/apache2/mods-enabled/ssl.load, placed before the template task that writes the vhost.


6. The fix in the playbook - validate, start, show the journal on failure

Putting it together: validate the configuration first, then start, and if either fails, print the journal before failing the play. This is block and rescue:

 1# safe.yml
 2- hosts: web
 3  tasks:
 4    - block:
 5        - name: Validate the Apache configuration first
 6          ansible.builtin.command: apache2ctl configtest
 7          changed_when: false
 8
 9        - name: Ensure Apache is running and enabled
10          ansible.builtin.service:
11            name: apache2
12            state: started
13            enabled: true
14
15      rescue:
16        - name: Show why it failed
17          ansible.builtin.command: journalctl -u apache2 --no-pager -n 15
18          register: jl
19        - debug: var=jl.stdout_lines
20        - fail: msg="Apache could not start - see the journal above"

With the typo back in place:

 1$ ansible-playbook -i inventory.ini safe.yml
 2
 3TASK [Validate the Apache configuration first] *********************************
 4fatal: [web]: FAILED! => {"changed": false, "cmd": ["apache2ctl", "configtest"], "delta": "0:00:00.019274", "rc": 1, "stderr_lines": ["AH00526: Syntax error on line 1 of /etc/apache2/conf-enabled/typo.conf:", "Invalid command 'Lisen', perhaps misspelled or defined by a module not included in the server configuration"]}
 5
 6TASK [Show why it failed] ******************************************************
 7changed: [web]
 8
 9TASK [debug] *******************************************************************
10    "jl.stdout_lines": [
11        "apachectl[2436]: AH00526: Syntax error on line 2 of /etc/apache2/sites-enabled/bad.conf:",
12        "apachectl[2436]: Invalid command 'SSLEngine', perhaps misspelled or defined by a module not included in the server configuration",
13        "systemd[1]: Failed to start apache2.service - The Apache HTTP Server.",
14        "systemd[1]: Started apache2.service - The Apache HTTP Server."
15    ]
16
17TASK [fail] ********************************************************************
18fatal: [web]: FAILED! => {"changed": false, "msg": "Apache could not start - see the journal above"}
19
20PLAY RECAP *********************************************************************
21web                        : ok=3    changed=1    unreachable=0    failed=1    skipped=0    rescued=1    ignored=0
22
23$ ansible-playbook -i inventory.ini safe.yml        # after removing typo.conf
24TASK [Validate the Apache configuration first] *********************************
25ok: [web]
26TASK [Ensure Apache is running and enabled] ************************************
27ok: [web]
28PLAY RECAP *********************************************************************
29web                        : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0

The configtest task failed in 19 ms, the service was never touched, and the play's own output contains the file and line to fix. changed_when: false keeps the validation from reporting changed on every run, and enabled: true makes the service come back after a reboot, which the original playbook forgot. For the restart-on-config-change half of this, Ansible handlers is the pattern, and journalctl options such as --since "10 minutes ago" are in journalctl(1).


7. What broke when I ran this

I ran this in an Ubuntu 24.04 systemd container (jrei/systemd-ubuntu:24.04, --privileged, cgroup v2 mounted) on a GitHub Actions runner on 2026-10-16, with Ansible connecting through the community.docker.docker connection plugin, the same setup as my Ansible lab on a laptop with Docker.

1. The first version of my block/rescue playbook did not parse. I generated it with a sed that re-indented the tasks and got YAML parsing failed: While parsing a block collection did not find expected '-' indicator at the register: jl line, which had ended up two spaces short. rescue: must be at the same indentation as block:, and every task under both must be indented consistently. I rewrote the file by hand (the version above) and re-ran; ansible-core 2.21 points at the exact line and column, which made it a 2-minute fix.

2. The journal in the rescue showed the previous failure too. In the output above the last 15 lines include the SSLEngine failure from cause 3 and a Started apache2.service line, because the journal is a history, not a status. Read the journal from the bottom up to the most recent Starting apache2.service line; everything above it is older.

3. The container needed 6 seconds before systemd would answer. systemctl immediately after docker run fails with a connection error; the script sleeps 6 seconds. On a freshly booted VM the equivalent is a wait_for_connection task before anything touches services.

4. AH00558 appears on every single start and means nothing here. The ServerName warning was in all three failures and in the healthy start. If your journal has only AH00558 and status=1/FAILURE, scroll further; the cause is on another line.


8. Conclusion

To summarise:

  1. "Unable to start service apache2 ... control process exited with error code" is three layers of relay; the cause is in journalctl -u apache2.
  2. AH00072 ... could not bind to address 0.0.0.0:80 is port 80 taken, ss -ltnp names the owner.
  3. AH00526 ... Invalid command is a typo or a module that is not enabled; apache2ctl configtest finds it in 18 ms.
  4. Run configtest as a task before the service task, inside a block, with a rescue that prints the journal.
  5. Add enabled: true so Apache survives the next reboot.

If Apache is not even installed yet, the Ansible installation guide and the Terraform and Ansible production web app cover the install-configure-start sequence end to end.

Did this save you a debugging session? Stay in the loop -